// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: fengyang

#pragma once

#include <cstdio>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

#include <cxxopts.hpp>

#include "appcoin2/support/symbology/py_currency.h"
#include "appcoin2/support/symbology/py_product_info.h"
#include "coin2/exchange/base/symbology/derivative_util.h"
#include "coin2/exchange/base/symbology/product.h"
#include "coin2/exchange/base/symbology/product_holder_manager.h"
#include "coin2/exchange/factory/symbology/product.h"

using coin2::exchange::base::market::MarketExchangeApi;
using coin2::exchange::base::symbology::ICurrency;
using coin2::exchange::base::symbology::IProduct;
using coin2::exchange::base::symbology::StringToOptionsSide;

namespace appcoin2::support::symbology {
using ProductHolderManager = coin2::exchange::base::symbology::ProductHolderManager;

class PyProduct {
 public:
  static PyProduct from_norm(const std::string& symbol, int64_t resolve_timestamp) {
    std::vector<std::string> out;
    out = Split(symbol, ":");
    if (out.size() != 3) {
      throw std::runtime_error(
          "CreateProductFromUniqueNormString fails: " + symbol + "," + std::to_string(__LINE__));
    }

    auto market = coin2::exchange::base::market::StringToMarketType(out[0]);
    if (!market) {
      throw std::runtime_error(
          "CreateProductFromUniqueNormString fails: " + symbol + "," + std::to_string(__LINE__));
    }

    auto exchange = coin2::exchange::base::market::StringToExchangeType(out[1]);
    if (!exchange) {
      throw std::runtime_error(
          "CreateProductFromUniqueNormString fails: " + symbol + "," + std::to_string(__LINE__));
    }

    const auto& product_str = out[2];
    auto product = CreateProductFromNormStringNoThrow(
        market.value(),
        exchange.value(),
        product_str,
        resolve_timestamp);
    if (!product) {
      throw std::runtime_error(
          "CreateProductFromUniqueNormString fails: " + symbol + "," + std::to_string(__LINE__));
    }
    return PyProduct(std::move(product));
  }

  static PyProduct from_native(const std::string& symbol, int64_t resolve_timestamp) {
    std::vector<std::string> out;
    out = Split(symbol, ":");
    if (out.size() != 4) {
      throw std::runtime_error("CreateProductFromUniqueNativeString fails: " + symbol);
    }

    auto market = coin2::exchange::base::market::StringToMarketType(out[0]);
    if (!market) {
      throw std::runtime_error("CreateProductFromUniqueNativeString fails: " + symbol);
    }

    auto exchange = coin2::exchange::base::market::StringToExchangeType(out[1]);
    if (!exchange) {
      throw std::runtime_error("CreateProductFromUniqueNativeString fails: " + symbol);
    }

    const auto& api = out[2];
    auto mea = MarketExchangeApi{market.value(), exchange.value(), api};
    const auto& product_str = out[3];
    auto product = CreateProductFromNativeStringNoThrow(mea, product_str, resolve_timestamp);
    if (!product) {
      throw std::runtime_error("CreateProductFromUniqueNativeString fails: " + symbol);
    }
    return PyProduct(std::move(product));
  }

  const PyProductHolder& product_holder() {
    return search_product_holder(default_fallback_days_);
  }

  const PyProductHolder& search_product_holder(int fallback_days) {
    if (product_holders_.count(fallback_days) == 0) {
      auto phm = GetProductHolderManager();
      phm->SetFallBackNDay(fallback_days);
      auto ph = phm->GetProductHolder(*product_, product_->resolve_timestamp());
      auto ph_ptr = std::unique_ptr<IProductHolder>(ph->Clone());
      product_holders_[fallback_days].reset(new PyProductHolder(std::move(ph_ptr)));
    }
    return *(product_holders_[fallback_days]);
  }
  std::string market_name() { return product_->market_name(); }
  std::string exchange_name() { return product_->exchange_name(); }
  std::string symbol() { return product_->symbol(); }
  std::string native_symbol() { return product_->native_symbol(); }
  std::string feed_native_symbol() { return product_->feed_native_symbol(); }
  std::string order_native_symbol() { return product_->order_native_symbol(); }
  std::string relative_norm() { return product_->relative_norm(); }
  std::string absolute_norm() { return product_->absolute_norm(); }
  std::string subscription_symbol() { return product_->relative_norm(); }
  std::string native_subscription_symbol() { return product_->feed_native_symbol(); }
  const PyCurrency& base() { return base_; }
  const PyCurrency& quote() { return quote_; }
  // coin1 interface
  std::string exchange() { return product_->exchange_name(); }
  std::string product_type() { return product_->market_name(); }
  std::string short_symbol() { return product_->symbol(); }

  std::string full_symbol() {
    return fmt::format(
        "{}-{}-{}",
        product_->symbol(),
        product_->exchange_name(),
        product_->native_symbol());
  }

  std::optional<std::string> contract_type() {
    if (product_->market() == coin::proto::MarketType::Futures) {
      std::string base_str;
      std::string quote_str;
      std::string contract_type_str;
      bool success =
          ParseFuturesNormStr(product_->relative_norm(), &base_str, &quote_str, &contract_type_str);
      if (!success) {
        throw std::runtime_error("ParseFuturesNormStr fails: " + full_symbol());
      }
      return contract_type_str;
    } else if (product_->market() == coin::proto::MarketType::Options) {
      std::string base_str;
      std::string quote_str;
      std::string contract_type_str;
      std::string side_str;
      std::string strike_price_str;
      std::string strike_price_multiplier_str;
      bool success = ParseOptionsNormStr(
          product_->absolute_norm(),
          &base_str,
          &quote_str,
          &contract_type_str,
          &side_str,
          &strike_price_str,
          &strike_price_multiplier_str);
      if (!success) {
        throw std::runtime_error("ParseOptionsNormStr fails: " + full_symbol());
      }
      return contract_type_str;
    }
    return std::nullopt;
  }

  std::optional<std::string> expiry_str() {
    if (product_->market() == coin::proto::MarketType::Futures) {
      std::string base_str;
      std::string quote_str;
      std::string expiry_str;
      bool success =
          ParseFuturesNormStr(product_->absolute_norm(), &base_str, &quote_str, &expiry_str);
      if (!success) {
        throw std::runtime_error("ParseFuturesNormStr fails: " + full_symbol());
      }
      return expiry_str;
    } else if (product_->market() == coin::proto::MarketType::Options) {
      std::string base_str;
      std::string quote_str;
      std::string expiry_str;
      std::string side_str;
      std::string strike_price_str;
      std::string strike_price_multiplier_str;
      bool success = ParseOptionsNormStr(
          product_->absolute_norm(),
          &base_str,
          &quote_str,
          &expiry_str,
          &side_str,
          &strike_price_str,
          &strike_price_multiplier_str);
      if (!success) {
        throw std::runtime_error("ParseOptionsNormStr fails: " + full_symbol());
      }
      // TODO(chensili): convert strike_price_str when multiplier is not 1
      return expiry_str;
    }
    return std::nullopt;
  }

  std::optional<double> strike_price() {
    if (product_->market() == coin::proto::MarketType::Options) {
      std::string base_str;
      std::string quote_str;
      std::string expiry_str;
      std::string side_str;
      std::string strike_price_str;
      std::string strike_price_multiplier_str;
      bool success = ParseOptionsNormStr(
          product_->absolute_norm(),
          &base_str,
          &quote_str,
          &expiry_str,
          &side_str,
          &strike_price_str,
          &strike_price_multiplier_str);
      if (!success) {
        throw std::runtime_error("ParseOptionsNormStr fails: " + full_symbol());
      }
      // TODO(chensili): convert strike_price_str when multiplier is not 1
      return std::stod(strike_price_str);
    }
    return std::nullopt;
  }

  std::optional<int> strike_price_multiplier() {
    if (product_->market() == coin::proto::MarketType::Options) {
      std::string base_str;
      std::string quote_str;
      std::string expiry_str;
      std::string side_str;
      std::string strike_price_str;
      std::string strike_price_multiplier_str;
      bool success = ParseOptionsNormStr(
          product_->absolute_norm(),
          &base_str,
          &quote_str,
          &expiry_str,
          &side_str,
          &strike_price_str,
          &strike_price_multiplier_str);
      if (!success) {
        throw std::runtime_error("ParseOptionsNormStr fails: " + full_symbol());
      }
      // TODO(chensili): convert strike_price_str when multiplier is not 1
      return std::stoi(strike_price_multiplier_str);
    }
    return std::nullopt;
  }

  std::optional<std::string> option_side() {
    if (product_->market() == coin::proto::MarketType::Options) {
      std::string base_str;
      std::string quote_str;
      std::string expiry_str;
      std::string side_str;
      std::string strike_price_str;
      std::string strike_price_multiplier_str;
      bool success = ParseOptionsNormStr(
          product_->absolute_norm(),
          &base_str,
          &quote_str,
          &expiry_str,
          &side_str,
          &strike_price_str,
          &strike_price_multiplier_str);
      if (!success) {
        throw std::runtime_error("ParseOptionsNormStr fails: " + full_symbol());
      }
      auto side = StringToOptionsSide(side_str);
      if (side.has_value()) {
        return OptionsSide_Name(side.value());
      }
    }
    return std::nullopt;
  }

  bool is_perpetual() {
    bool ret = false;
    if (product_->market() == coin::proto::MarketType::Futures) {
      auto contract_type_str = contract_type();
      if (contract_type_str.has_value() && contract_type_str.value() == "PERPETUAL") {
        ret = true;
      }
    }
    return ret;
  }

  bool is_immediate() {
    bool ret = false;
    if (product_->market() == coin::proto::MarketType::Futures) {
      auto contract_type_str = contract_type();
      if (contract_type_str.has_value() && contract_type_str.value() == "IMMEDIATE") {
        ret = true;
      }
    }
    return ret;
  }

 private:
  explicit PyProduct(std::unique_ptr<IProduct> product)
      : product_(std::move(product)), base_(product_->base()), quote_(product_->quote()) {}

  static bool ParseFuturesNormStr(
      const std::string& futures_norm_str,
      std::string* base,
      std::string* quote,
      std::string* expiry_str) {
    std::vector<std::string> out;
    out = Split(futures_norm_str, "-.");
    if (out.size() == 3 || out.size() == 4) {
      *base = out.at(0);
      *quote = out.at(1);
      *expiry_str = out.at(2);
      return true;
    }
    return false;
  }

  static bool ParseOptionsNormStr(
      const std::string& options_norm_str,
      std::string* base,
      std::string* quote,
      std::string* expiry_str,
      std::string* side_str,
      std::string* strike_price_str,
      std::string* strike_price_multiplier_str) {
    std::vector<std::string> out;
    out = Split(options_norm_str, "-.");
    if (out.size() == 5 || out.size() == 6) {
      *base = out.at(0);
      *quote = out.at(1);
      *expiry_str = out.at(2);
      *side_str = out.at(3);
      *strike_price_str = out.at(4);
      *strike_price_multiplier_str = out.at(5);
      // Ignore out[5] as strike_price_multiplier.
      return true;
    }
    return false;
  }

  using ProductHolderManager = coin2::exchange::base::symbology::ProductHolderManager;
  static ProductHolderManager* GetProductHolderManager() {
    static std::unique_ptr<ProductHolderManager> phm;
    if (phm == nullptr) {
      auto config = coin::proto::ProductHolderManagerConfig();
      phm.reset(new ProductHolderManager(config));
    }
    return phm.get();
  }

  const int32_t default_fallback_days_{7};
  std::unique_ptr<IProduct> product_;
  std::unordered_map<uint32_t, std::unique_ptr<PyProductHolder>> product_holders_;
  const PyCurrency base_;
  const PyCurrency quote_;
};
}  // namespace appcoin2::support::symbology
